using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Binance.Client.Websocket.Exceptions;
using Binance.Client.Websocket.Signing;
using Newtonsoft.Json;

namespace Binance.Client.Websocket.Rest
{
    /// <summary>
    /// Binance base class for REST API.
    /// </summary>
    public abstract class BinanceRestApi
    {
        private static readonly string UserAgent = "binance-client-websocket-dotnet/1.0";
        private readonly string? _apiKey;
        private readonly IBinanceSignatureService _signatureService;
        private readonly string _baseUrl;
        private readonly HttpClient _httpClient;

        public BinanceRestApi(HttpClient httpClient, string baseUrl, string? apiKey, string? apiSecret)
        {
            _httpClient = httpClient;
            _baseUrl = baseUrl;
            _apiKey = apiKey;
            _signatureService = new BinanceHmac(apiSecret);
        }

        public BinanceRestApi(HttpClient httpClient, string baseUrl, string? apiKey, IBinanceSignatureService signatureService)
        {
            _httpClient = httpClient;
            _baseUrl = baseUrl;
            _apiKey = apiKey;
            _signatureService = signatureService;
        }

        protected async Task<T> SendPublicAsync<T>(string requestUri, HttpMethod httpMethod, Dictionary<string, object>? query = null, object? content = null)
        {
            if (!(query is null))
            {
                StringBuilder queryStringBuilder = BuildQueryString(query, new StringBuilder());

                if (queryStringBuilder.Length > 0)
                {
                    requestUri += "?" + queryStringBuilder;
                }
            }

            return await SendAsync<T>(requestUri, httpMethod, content);
        }

        protected async Task<T> SendSignedAsync<T>(string requestUri, HttpMethod httpMethod, Dictionary<string, object>? query = null, object? content = null)
        {
            StringBuilder queryStringBuilder = new StringBuilder();

            if (!(query is null))
            {
                queryStringBuilder = BuildQueryString(query, queryStringBuilder);
            }

            var signature = _signatureService.Sign(queryStringBuilder.ToString());

            if (queryStringBuilder.Length > 0)
            {
                queryStringBuilder.Append("&");
            }

            queryStringBuilder.Append("signature=").Append(HttpUtility.UrlEncode(signature));

            requestUri += "?" + queryStringBuilder;

            return await SendAsync<T>(requestUri, httpMethod, content);
        }

        private StringBuilder BuildQueryString(Dictionary<string, object> queryParameters, StringBuilder builder)
        {
            foreach (KeyValuePair<string, object> queryParameter in queryParameters)
            {
                var queryParameterValue = Convert.ToString(queryParameter.Value, CultureInfo.InvariantCulture);
                if (!string.IsNullOrWhiteSpace(queryParameterValue))
                {
                    if (builder.Length > 0)
                    {
                        builder.Append("&");
                    }

                    builder
                        .Append(queryParameter.Key)
                        .Append("=")
                        .Append(HttpUtility.UrlEncode(queryParameterValue));
                }
            }

            return builder;
        }

        private async Task<T> SendAsync<T>(string requestUri, HttpMethod httpMethod, object? content = null)
        {
            using var request = new HttpRequestMessage(httpMethod, _baseUrl + requestUri);
            request.Headers.Add("User-Agent", UserAgent);
            if (!(content is null))
            {
                request.Content = new StringContent(JsonConvert.SerializeObject(content), Encoding.UTF8, "application/json");
            }

            if (!(_apiKey is null))
            {
                request.Headers.Add("X-MBX-APIKEY", _apiKey);
            }

            var response = await _httpClient.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                using HttpContent responseContent = response.Content;
                string jsonString = await responseContent.ReadAsStringAsync();

                if (typeof(T) == typeof(string))
                {
                    return (T)(object)jsonString;
                }

                try
                {
                    var data = JsonConvert.DeserializeObject<T>(jsonString);
                    return data;
                }
                catch (JsonReaderException ex)
                {
                    var clientException = new BinanceClientException($"Failed to map server response from '${requestUri}' to given type", -1, ex);

                    clientException.StatusCode = (int)response.StatusCode;
                    clientException.Headers = response.Headers.ToDictionary(a => a.Key, a => a.Value);

                    throw clientException;
                }
            }
                
            using var errorResponseContent = response.Content;
            BinanceHttpException? httpException = null;
            var contentString = await errorResponseContent.ReadAsStringAsync();
            var statusCode = (int)response.StatusCode;
            if (400 <= statusCode && statusCode < 500)
            {
                if (string.IsNullOrWhiteSpace(contentString))
                {
                    httpException = new BinanceClientException("Unsuccessful response with no content", -1);
                }
                else
                {
                    try
                    {
                        httpException = JsonConvert.DeserializeObject<BinanceClientException>(contentString);
                    }
                    catch (JsonReaderException ex)
                    {
                        httpException = new BinanceClientException(contentString, -1, ex);
                    }
                }
            }
            else
            {
                httpException = new BinanceServerException(contentString);
            }

            if (httpException != null)
            {
                httpException.StatusCode = statusCode;
                httpException.Headers = response.Headers.ToDictionary(a => a.Key, a => a.Value);

                throw httpException;
            }

            throw new InvalidOperationException();
        }
    }
}